﻿namespace Jsl.StyleCop.CSharp
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using Microsoft.StyleCop;
    using Microsoft.StyleCop.CSharp;

    /// <summary>
    /// A helper utility for working with the StyleCop abstract syntax trees (AST).
    /// </summary>
    public static class AstHelper
    {
        #region Public Methods
        /// <summary>
        /// Enumerates the code unit's ancestors.
        /// </summary>
        /// <param name="codeUnit">The code unit.</param>
        /// <returns>The code unit's ancestors.</returns>
        public static IEnumerable<ICodeUnit> Ancestors(this ICodeUnit codeUnit)
        {
            ICodeUnit ancestor = codeUnit.Parent;
            while (ancestor != null)
            {
                yield return ancestor;

                ancestor = ancestor.Parent;
            }
        }

        /// <summary>
        /// Enumerates the code unit's ancestors include the given code unit to the containing <see cref="CsElement"/>.
        /// </summary>
        /// <param name="codeUnit">The code unit.</param>
        /// <returns>The code unit's ancestors to the containing <see cref="CsElement"/>.</returns>
        public static IEnumerable<ICodeUnit> AncestorsIncludingThisToElement(this ICodeUnit codeUnit)
        {
            yield return codeUnit;

            foreach (ICodeUnit ancestor in codeUnit.AncestorsToElement())
            {
                yield return ancestor;
            }
        }

        /// <summary>
        /// Enumerates the code unit's ancestors to the containing <see cref="CsElement"/>.
        /// </summary>
        /// <param name="codeUnit">The code unit.</param>
        /// <returns>The code unit's ancestors to the containing <see cref="CsElement"/>.</returns>
        public static IEnumerable<ICodeUnit> AncestorsToElement(this ICodeUnit codeUnit)
        {
            foreach (ICodeUnit ancestor in codeUnit.Ancestors())
            {
                if (ancestor.CodeUnitType == CodeUnitType.Element)
                {
                    yield return ancestor.GetActualCsElement();
                    break;
                }
                else
                {
                    yield return ancestor;
                }
            }
        }

        /// <summary>
        /// Enumerates the children for the specified parent.
        /// </summary>
        /// <param name="parent">The parent.</param>
        /// <returns>The children for the specified parent.</returns>
        public static IEnumerable<ICodeUnit> Children(this CsDocument parent)
        {
            return new ICodeUnit[] { parent.RootElement }
                .Concat(parent.RootElement.Children());
        }

        /// <summary>
        /// Enumerates the children for the specified parent.
        /// </summary>
        /// <param name="parent">The parent.</param>
        /// <returns>The children for the specified parent.</returns>
        public static IEnumerable<ICodeUnit> Children(this ICodeUnit parent)
        {
            CodeElement codeElement = parent as CodeElement;
            if (codeElement != null)
            {
                foreach (CsElement child in codeElement.ChildCodeElements)
                {
                    if (child != null)
                    {
                        yield return child;
                    }
                }
            }

            ICodeUnit codeUnit = parent as ICodeUnit;
            if (codeUnit != null)
            {
                foreach (var child in codeUnit.ChildExpressions)
                {
                    if (child != null)
                    {
                        yield return child;
                    }
                }

                foreach (var child in codeUnit.ChildStatements)
                {
                    if (child != null)
                    {
                        yield return child;
                    }
                }
            }
        }

        /// <summary>
        /// Gets the closest comman ancestor of the two code units.
        /// </summary>
        /// <param name="codeUnit1">The code unit1.</param>
        /// <param name="codeUnit2">The code unit2.</param>
        /// <returns>The closest comman ancestor of the two code units.</returns>
        /// <remarks>
        /// This may return either of the two code units passed in if one is a child of the other.
        /// </remarks>
        public static ICodeUnit ClosestCommonAncestor(this ICodeUnit codeUnit1, ICodeUnit codeUnit2)
        {
            foreach (ICodeUnit ancestor1 in codeUnit1.AncestorsIncludingThisToElement())
            {
                foreach (ICodeUnit ancestor2 in codeUnit2.AncestorsIncludingThisToElement())
                {
                    if (object.ReferenceEquals(ancestor1, ancestor2))
                    {
                        return ancestor1;
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Gets the containing element.
        /// </summary>
        /// <param name="codeUnit">The code unit.</param>
        /// <returns>The containing element.</returns>
        public static CsElement ContainingElement(this ICodeUnit codeUnit)
        {
            return (CsElement)codeUnit.AncestorsToElement().Last();
        }

        /// <summary>
        /// Enumerates the descendants for the specified parent.
        /// </summary>
        /// <param name="parent">The parent.</param>
        /// <returns>The descendants for the specified parent.</returns>
        public static IEnumerable<ICodeUnit> Descendants(this CsDocument parent)
        {
            return new ICodeUnit[] { parent.RootElement }
                .Concat(parent.RootElement.Descendants());
        }

        /// <summary>
        /// Enumerates the descendants for the specified parent.
        /// </summary>
        /// <param name="parent">The parent.</param>
        /// <returns>The descendants for the specified parent.</returns>
        public static IEnumerable<ICodeUnit> Descendants(this ICodeUnit parent)
        {
            foreach (var child in parent.Children())
            {
                yield return child;

                foreach (var descendant in child.Descendants())
                {
                    yield return descendant;
                }
            }
        }

        /// <summary>
        /// Gets the statements, if any, that follow the given statement.
        /// </summary>
        /// <param name="statement">The statement.</param>
        /// <returns>The statements that follow the given statement.</returns>
        public static IEnumerable<Statement> FollowingStatements(this Statement statement)
        {
            bool pastStatement = false;
            foreach (Statement siblingStatement in statement.SiblingStatements())
            {
                if (pastStatement)
                {
                    yield return siblingStatement;
                }
                else if (object.ReferenceEquals(statement, siblingStatement))
                {
                    pastStatement = true;
                }
            }
        }

        /// <summary>
        /// Gets the <see cref="Variable"/> from the <see cref="VariableDeclaratorExpression"/>.
        /// </summary>
        /// <param name="variableDeclarator">The variable declarator.</param>
        /// <returns>The <see cref="Variable"/>.</returns>
        public static Variable GetVariable(this VariableDeclaratorExpression variableDeclarator)
        {
            if (variableDeclarator.ParentStatement is CatchStatement)
            {
                return variableDeclarator.ParentStatement.Variables[variableDeclarator.Identifier.Text];
            }

            return variableDeclarator.ParentStatement.Parent.Variables[variableDeclarator.Identifier.Text];
        }

        /// <summary>
        /// Gets the variable usage.
        /// </summary>
        /// <param name="literalExpression">The literal expression.</param>
        /// <returns>The variable usage.</returns>
        /// <remarks>
        /// The exception variable declared in a catch statement is counted as a declaration and assignment.
        /// </remarks>
        public static VariableUsageInformation GetVariableUsage(this LiteralExpression literalExpression)
        {
            // See if this is actually a variable.
            Variable variable;
            ICodeUnit hoistedInside;
            if (!literalExpression.TryGetVariable(out variable, out hoistedInside))
            {
                return null;
            }

            // The catch statements exception variable is counted as a declaration and assignment.
            CatchStatement catchStatement = literalExpression.ParentStatement as CatchStatement;
            if (catchStatement != null)
            {
                return new VariableUsageInformation(literalExpression, variable, VariableUsage.DeclarationAndAssignment, hoistedInside);
            }

            // See if it is the variable being declared.
            VariableDeclaratorExpression variableDeclarator = literalExpression.Parent as VariableDeclaratorExpression;
            if (variableDeclarator != null)
            {
                if (object.ReferenceEquals(variableDeclarator.Identifier, literalExpression))
                {
                    if (variableDeclarator.Initializer == null)
                    {
                        return new VariableUsageInformation(literalExpression, variable, VariableUsage.Declaration, hoistedInside);
                    }
                    else
                    {
                        return new VariableUsageInformation(literalExpression, variable, VariableUsage.DeclarationAndAssignment, hoistedInside);
                    }
                }
            }

            // The variable name might be the same as a field name so weed out the member accesses.
            MemberAccessExpression memberAccess = literalExpression.Parent as MemberAccessExpression;
            if (memberAccess != null)
            {
                if (object.ReferenceEquals(memberAccess.LeftHandSide, literalExpression))
                {
                    return new VariableUsageInformation(literalExpression, variable, VariableUsage.Access, hoistedInside);
                }
                else
                {
                    return null;
                }
            }

            // See if it is on the left side of an assignment statement.
            AssignmentExpression assignment = literalExpression.Parent as AssignmentExpression;
            if (assignment != null)
            {
                if (object.ReferenceEquals(assignment.LeftHandSide, literalExpression))
                {
                    if (assignment.IsAssignmentWithSelfReference())
                    {
                        return new VariableUsageInformation(literalExpression, variable, VariableUsage.AccessAndAssignment, hoistedInside);
                    }
                    else
                    {
                        return new VariableUsageInformation(literalExpression, variable, VariableUsage.Assignment, hoistedInside);
                    }
                }
            }

            // See if it is an out parameter.
            MethodInvocationExpression methodInvocation = literalExpression.Parent as MethodInvocationExpression;
            if (methodInvocation != null)
            {
                if (methodInvocation.IsOutParameter(literalExpression))
                {
                    return new VariableUsageInformation(literalExpression, variable, VariableUsage.OutParameter, hoistedInside);
                }
                else if (methodInvocation.IsRefParameter(literalExpression))
                {
                    return new VariableUsageInformation(literalExpression, variable, VariableUsage.RefParameter, hoistedInside);
                }
            }

            // If its not an assignment or out parameter then the only other possibility is access.
            return new VariableUsageInformation(literalExpression, variable, VariableUsage.Access, hoistedInside);
        }

        /// <summary>
        /// Formats the location for display in errors, etc.
        /// </summary>
        /// <param name="location">The location.</param>
        /// <returns>The location formatted for reading.</returns>
        public static string HumanReadable(this CodeLocation location)
        {
            return
                string.Format(
                    CultureInfo.CurrentCulture,
                    "({0}, {1})",
                    location.StartPoint.LineNumber,
                    location.StartPoint.IndexOnLine);
        }

        /// <summary>
        /// Determines if the two code units are in the same element.
        /// </summary>
        /// <param name="item">The item to check.</param>
        /// <param name="target">The target to search for.</param>
        /// <returns><c>true</c> if the two code units are in the same element.</returns>
        public static bool InSameElement(this ICodeUnit item, ICodeUnit target)
        {
            return object.ReferenceEquals(item.ParentElement(), target.ParentElement());
        }

        /// <summary>
        /// Determines whether the <see cref="AssignmentExpression"/> is like +=, -=, *=, etc.
        /// </summary>
        /// <param name="assignment">The assignment.</param>
        /// <returns><c>true</c> if the <see cref="AssignmentExpression"/> is like +=, -=, *=, etc.</returns>
        public static bool IsAssignmentWithSelfReference(this AssignmentExpression assignment)
        {
            switch (assignment.OperatorType)
            {
                case AssignmentExpression.Operator.AndEquals:
                case AssignmentExpression.Operator.DivisionEquals:
                case AssignmentExpression.Operator.LeftShiftEquals:
                case AssignmentExpression.Operator.MinusEquals:
                case AssignmentExpression.Operator.ModEquals:
                case AssignmentExpression.Operator.MultiplicationEquals:
                case AssignmentExpression.Operator.OrEquals:
                case AssignmentExpression.Operator.PlusEquals:
                case AssignmentExpression.Operator.RightShiftEquals:
                case AssignmentExpression.Operator.XorEquals:
                    return true;

                case AssignmentExpression.Operator.Equals:
                    return false;

                default:
                    throw new InvalidOperationException("Unknown assignment operator type.");
            }
        }

        /// <summary>
        /// Determines whether the expression is a boolean constant.
        /// </summary>
        /// <param name="expression">The expression.</param>
        /// <returns><c>true</c> if the expression is a boolean constant.</returns>
        public static bool IsBooleanConstant(this Expression expression)
        {
            LiteralExpression literalExpression = expression as LiteralExpression;
            if (literalExpression != null)
            {
                return literalExpression.IsBooleanConstant();
            }

            return false;
        }

        /// <summary>
        /// Determines whether the expression is a boolean constant.
        /// </summary>
        /// <param name="literalExpression">The literal expression.</param>
        /// <returns><c>true</c> if the expression is a boolean constant.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "We only want LiteralExpressions.")]
        public static bool IsBooleanConstant(this LiteralExpression literalExpression)
        {
            return (literalExpression.Text == "true") || (literalExpression.Text == "false");
        }

        /// <summary>
        /// Determines whether if the item is a decendant of the specified ancestor.
        /// </summary>
        /// <param name="codeUnit">The code unit.</param>
        /// <param name="ancestor">The ancestor.</param>
        /// <returns><c>true</c> if the item is decendant of the specified ancestor.</returns>
        public static bool IsDescendantOf(this ICodeUnit codeUnit, ICodeUnit ancestor)
        {
            return codeUnit.Ancestors().Any(item => object.ReferenceEquals(item, ancestor));
        }

        /// <summary>
        /// Determines whether literal expression is an out or ref paramter.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="variable">The variable to check.</param>
        /// <returns><c>true</c> if literal expression is an out or ref paramter.</returns>
        public static bool IsOutOrRefParameter(this CsElement element, Variable variable)
        {
            IParameterContainer parameterContainer = element as IParameterContainer;
            if (parameterContainer != null)
            {
                Parameter parameter = parameterContainer
                    .Parameters
                    .Where(param => string.Equals(param.Name, variable.Name, StringComparison.Ordinal))
                    .FirstOrDefault();
                if (parameter != null)
                {
                    return (parameter.Modifiers & (ParameterModifiers.Out | ParameterModifiers.Ref)) != ParameterModifiers.None;
                }
            }

            return false;
        }

        /// <summary>
        /// Determines whether the literal expression is being used as an out parameter.
        /// </summary>
        /// <param name="methodInvocation">The method invocation.</param>
        /// <param name="literalExpression">The literal expression.</param>
        /// <returns><c>true</c> if the literal expression is being used as an out parameter.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "We are specifically only looking at method invocations.")]
        public static bool IsOutParameter(this MethodInvocationExpression methodInvocation, LiteralExpression literalExpression)
        {
            /* TODO: Find a better way of doing this. */

            int tokenIndex = 0;
            int lastOutToken = int.MaxValue;
            foreach (Token token in methodInvocation.Tokens)
            {
                if (token.Text == "out")
                {
                    lastOutToken = tokenIndex;
                }
                else if (token.Text == literalExpression.Text)
                {
                    if (tokenIndex - 2 == lastOutToken)
                    {
                        return true;
                    }
                }

                tokenIndex++;
            }

            return false;
        }

        /// <summary>
        /// Determines whether the specified element has the readonly keyword.
        /// </summary>
        /// <param name="element">The element to check.</param>
        /// <returns><c>true</c> if the specified element has the readonly keyword.</returns>
        public static bool IsReadOnly(this CsElement element)
        {
            return element.Tokens.Any(token => token.CsTokenType == CsTokenType.Readonly);
        }

        /// <summary>
        /// Determines whether the literal expression is being used as a ref parameter.
        /// </summary>
        /// <param name="methodInvocation">The method invocation.</param>
        /// <param name="literalExpression">The literal expression.</param>
        /// <returns><c>true</c> if the literal expression is being used as a ref parameter.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "The parameter types are as needed.")]
        public static bool IsRefParameter(this MethodInvocationExpression methodInvocation, LiteralExpression literalExpression)
        {
            /* TODO: Find a better way of doing this. */

            int tokenIndex = 0;
            int lastRefToken = int.MaxValue;
            foreach (Token token in methodInvocation.Tokens)
            {
                if (token.Text == "ref")
                {
                    lastRefToken = tokenIndex;
                }
                else if (token.Text == literalExpression.Text)
                {
                    if (tokenIndex - 2 == lastRefToken)
                    {
                        return true;
                    }
                }

                tokenIndex++;
            }

            return false;
        }

        /// <summary>
        /// Determines whether the specified element is static.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <returns><c>true</c> if the specified element is static.</returns>
        public static bool IsStatic(this CsElement element)
        {
            return element.Tokens.Any(token => token.CsTokenType == CsTokenType.Static);
        }

        /// <summary>
        /// Determines if the parent element is a class or struct.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <returns><c>true</c> if the parent element is a class or struct.</returns>
        public static bool IsType(this CsElement element)
        {
            ElementType elementType = element.ElementType;
            return elementType == ElementType.Class || elementType == ElementType.Struct;
        }

        /// <summary>
        /// Determines whether the specified literal expression is a variable.
        /// </summary>
        /// <param name="literalExpression">The literal expression.</param>
        /// <returns><c>true</c> if the specified literal expression is a variable.</returns>
        public static bool IsVariable(this LiteralExpression literalExpression)
        {
            Variable variable;
            return literalExpression.TryGetVariable(out variable);
        }

        /// <summary>
        /// Determines if the <see cref="LiteralExpression"/> is a variable being set.
        /// </summary>
        /// <param name="literalExpression">The literal expression that may be a variable.</param>
        /// <returns><c>true</c> if the literal expression is a variable being set.</returns>
        /// <remarks>
        /// A variable can be set in an assignment, when used as an out parameter and in its declaration with an initializer.
        /// </remarks>
        public static bool IsVariableSetting(this LiteralExpression literalExpression)
        {
            VariableUsageInformation variableUsage = literalExpression.GetVariableUsage();
            if (variableUsage != null)
            {
                return variableUsage.Usage.IsVariableSetting();
            }

            return false;
        }

        /// <summary>
        /// Determines whether the usage is setting the variable.
        /// </summary>
        /// <param name="usage">The usage.</param>
        /// <returns><c>true</c> if the usage is a variable being set.</returns>
        public static bool IsVariableSetting(this VariableUsage usage)
        {
            switch (usage)
            {
                case VariableUsage.Access:
                case VariableUsage.Declaration:
                    return false;

                case VariableUsage.AccessAndAssignment:
                case VariableUsage.Assignment:
                case VariableUsage.DeclarationAndAssignment:
                case VariableUsage.OutParameter:
                case VariableUsage.RefParameter:
                    return true;

                default:
                    throw new InvalidOperationException(
                        string.Format(CultureInfo.CurrentUICulture, "Unknown variable usage at line {1}.", usage));
            }
        }

        /// <summary>
        /// Gets the parent element of this item.
        /// </summary>
        /// <param name="codeUnit">The code unit.</param>
        /// <returns>The parent element of this item.</returns>
        /// <remarks>
        /// The <see cref="ICodeUnit.ParentElement"/> throws exceptions when called from inside a method, and possibly
        /// other places. The issue is that the <see cref="ICodeUnit.Parent"/> isn't a <see cref="Method"/> but a
        /// <see cref="CodeUnit"/> which isn't derived from <see cref="CsElement"/>.
        /// </remarks>
        public static CsElement ParentElement(this ICodeUnit codeUnit)
        {
            return (CsElement)codeUnit.AncestorsToElement().Last();
        }

        /// <summary>
        /// Enumerates the siblings statements for the given statement.
        /// </summary>
        /// <param name="statement">The statement.</param>
        /// <returns>The siblings statements for the given statement.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "We are specifically looking at statements.")]
        public static IEnumerable<Statement> SiblingStatements(this Statement statement)
        {
            return statement.Parent.ChildStatements;
        }

        /// <summary>
        /// Tries to get variable from the <see cref="LiteralExpression"/>.
        /// </summary>
        /// <param name="literalExpression">The literal expression.</param>
        /// <param name="variable">The variable.</param>
        /// <returns><c>true</c> if the <see cref="LiteralExpression"/> is a variable usage.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "All the other variable related methods take a LiteralExpression.")]
        public static bool TryGetVariable(this LiteralExpression literalExpression, out Variable variable)
        {
            ICodeUnit hoistedInside;
            return literalExpression.TryGetVariable(out variable, out hoistedInside);
        }

        /// <summary>
        /// Tries to get variable from the <see cref="LiteralExpression"/>.
        /// </summary>
        /// <param name="literalExpression">The literal expression.</param>
        /// <param name="variable">The variable.</param>
        /// <param name="hoistedInside">The code unit the variable is hoisted inside.</param>
        /// <returns><c>true</c> if the <see cref="LiteralExpression"/> is a variable usage.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "This follows the TryGetValue pattern.")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "All the other variable related methods take a LiteralExpression.")]
        public static bool TryGetVariable(this LiteralExpression literalExpression, out Variable variable, out ICodeUnit hoistedInside)
        {
            hoistedInside = null;

            // Walk up the ancestor tree until we find where/if we are declared.
            foreach (ICodeUnit ancestor in literalExpression.AncestorsToElement())
            {
                // Return the variable if it was found.
                variable = ancestor.Variables[literalExpression.Text];
                if (variable != null)
                {
                    return true;
                }

                // If the variable usage was inside a lambda expression of an anonymous method then the variable was hoisted
                // since we haven't got to the level it is declared at.
                if (hoistedInside == null)
                {
                    if ((ancestor is LambdaExpression)
                        || (ancestor is AnonymousMethodExpression))
                    {
                        hoistedInside = ancestor;
                    }
                }
            }

            variable = null;
            return false;
        }
        #endregion Public Methods

        #region Private Methods
        private static CsElement GetActualCsElement(this ICodeUnit codeUnit)
        {
            if (codeUnit.CodeUnitType != CodeUnitType.Element)
            {
                throw new ArgumentException("Expected an ICodeUnit with CodeUnitType of Element.");
            }

            // If this is actually a CsElement then just return it.
            CsElement element = codeUnit as CsElement;
            if (element != null)
            {
                return element;
            }

            CsElement elementParent = codeUnit.Parent as CsElement;
            if (elementParent == null)
            {
                throw new InvalidOperationException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        "The parent of a CsElement should be a CsElement at position {0}.",
                        codeUnit.Location));
            }

            // The child element of the elementParent that has the same CodeLocation should be the element we are looking for.
            return
                (from siblingElement in elementParent.ChildElements
                 where siblingElement.Location == codeUnit.Location
                 select siblingElement).Single();
        }
        #endregion Private Methods
    }
}